Skip to content

feat(support): unicode-aware glob matching with byte fallback - #179

Open
16bit-ykiko wants to merge 1 commit into
mainfrom
feat/glob-unicode
Open

feat(support): unicode-aware glob matching with byte fallback#179
16bit-ykiko wants to merge 1 commit into
mainfrom
feat/glob-unicode

Conversation

@16bit-ykiko

@16bit-ykiko 16bit-ykiko commented Jul 21, 2026

Copy link
Copy Markdown
Member

Problem

GlobPattern matched byte-by-byte with a bitset<256> character class, as its own doc comment admitted: multi-byte UTF-8 characters were never matched by ? (one byte each) and [...] ranges over non-ASCII were meaningless. VS Code's own glob engine matches ? against a whole character, so patterns over CJK paths behaved differently from the syntax's reference implementation.

Approach

The matcher strategy follows rust-lang/glob (matching over decoded code points) combined with glibc fnmatch's precedent of falling back to bytes for invalid sequences:

  • ?, [...] and \-escaped literals now consume one decoded UTF-8 code point at a time.
  • Character classes store [lo, hi] code-point ranges instead of a byte bitset; negation is evaluated at match time (a bracket still never matches /).
  • A byte that does not form valid UTF-8 (bad lead, truncated/overlong sequence, surrogate) is treated as a single-byte atom that only compares equal to itself — non-UTF-8 paths keep matching literally, ? consumes exactly one such byte, and nothing crashes or mis-aligns.
  • *, **, literal comparison and backtracking stay byte-level on purpose: UTF-8 is self-synchronizing and / is ASCII, so byte stepping cannot change match semantics there.

Brace expansion, segment handling, prefix extraction and the ReDoS backtrack cap are untouched.

Testing

  • New suites: unicode_question, unicode_bracket, unicode_escape_star, invalid_utf8_bytes, plus ported_wildcards/ported_ranges adapted from rust-lang/glob's test suite (MIT/Apache-2.0).
  • Full local run: 1373 unit tests and 34 integration tests pass.

Summary by CodeRabbit

  • New Features

    • Improved glob matching for Unicode characters and multibyte UTF-8 text.
    • Added support for Unicode ranges and negated character classes.
    • Invalid UTF-8 bytes are handled consistently as individual literals.
  • Bug Fixes

    • Corrected wildcard, escaped-character, and bracket matching across Unicode and UTF-8 edge cases.
  • Tests

    • Added coverage for Unicode patterns, invalid UTF-8, wildcard behavior, and character-range edge cases.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Glob matching now decodes valid UTF-8 into Unicode code points for wildcards, brackets, and escaped literals. Brackets store Unicode ranges with negation support, while invalid UTF-8 bytes remain single-byte literals. Tests cover Unicode, malformed input, wildcard, and range behavior.

Changes

Unicode glob matching

Layer / File(s) Summary
Glob matching contract
include/kota/support/glob_pattern.h
Documents UTF-8 atom semantics and replaces the byte bitset with negated Unicode code-point ranges.
UTF-8 and bracket parsing
src/support/glob_pattern.cpp
Validates UTF-8 atoms, parses bracket expressions into inclusive code-point ranges, and preserves negation and segment-boundary rules.
Unicode matching and validation
src/support/glob_pattern.cpp, tests/unit/support/glob_pattern_tests.cpp
Updates ?, brackets, and escaped literals to consume decoded atoms, with tests for Unicode, invalid UTF-8, wildcard, and range cases.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Pattern as GlobPattern::SubGlobPattern::create
  participant Decoder as UTF-8 atom decoder
  participant Matcher as glob match engine
  Pattern->>Decoder: Decode bracket and escaped pattern atoms
  Decoder-->>Pattern: Valid code points or literal invalid bytes
  Pattern->>Matcher: Store Unicode ranges and negation
  Matcher->>Decoder: Decode input atom
  Decoder-->>Matcher: Code point and byte length
  Matcher->>Matcher: Apply wildcard, bracket, or escaped-literal match
Loading

Possibly related PRs

  • clice-io/kotatsu#126: Introduced the GlobPattern implementation whose byte-based bracket and wildcard logic is replaced here.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: Unicode-aware glob matching with byte fallback for invalid UTF-8.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/glob-unicode

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/support/glob_pattern.cpp (1)

370-374: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Minor: inconsistent error-propagation style.

The rest of create()/its lambdas use KOTA_EXPECTED_TRY_V (e.g. line 104, 314, 317) for std::expected propagation, but this spot manually checks has_value()/error(). Purely stylistic.

♻️ Optional consistency tweak
-        auto ranges = parse_bracket_charset(invert ? chars.substr(1) : chars);
-        if(!ranges.has_value()) [[unlikely]] {
-            return std::unexpected{std::move(ranges.error())};
-        }
-        pat.brackets.push_back(Bracket{j + 1, invert, std::move(*ranges)});
+        KOTA_EXPECTED_TRY_V(auto ranges, parse_bracket_charset(invert ? chars.substr(1) : chars));
+        pat.brackets.push_back(Bracket{j + 1, invert, std::move(ranges)});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/support/glob_pattern.cpp` around lines 370 - 374, In the bracket parsing
flow of create(), replace the manual ranges.has_value() check and
std::unexpected error return with the existing KOTA_EXPECTED_TRY_V propagation
style, while preserving the resulting ranges value for the subsequent
pat.brackets.push_back call.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/support/glob_pattern.cpp`:
- Around line 370-374: In the bracket parsing flow of create(), replace the
manual ranges.has_value() check and std::unexpected error return with the
existing KOTA_EXPECTED_TRY_V propagation style, while preserving the resulting
ranges value for the subsequent pat.brackets.push_back call.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2ef67048-0ca9-4682-971c-338cea36ae1a

📥 Commits

Reviewing files that changed from the base of the PR and between f2cdf65 and 1f380af.

📒 Files selected for processing (3)
  • include/kota/support/glob_pattern.h
  • src/support/glob_pattern.cpp
  • tests/unit/support/glob_pattern_tests.cpp

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

s = ++state.s;

P2 Badge Advance wildcard backtracking by UTF-8 atoms

When * backtracks over a valid multibyte character, incrementing state.s by one byte lets the next matcher start on a continuation byte. That byte is then decoded as a standalone invalid atom, violating the documented rule that invalid bytes only equal themselves; for example, a pattern consisting of "*[" + byte(0xA9) + "]" (or an escaped/literal 0xA9) incorrectly matches the valid UTF-8 subject "é" (C3 A9) after * consumes only C3. Advance to the next decoded atom boundary instead of testing positions inside valid UTF-8 sequences.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +431 to +433
const bool hit = std::ranges::any_of(ranges, [&](const auto& range) {
return range.first <= cp && cp <= range.second;
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bound or normalize character-class ranges

For a pattern containing * followed by a large character class, every wildcard backtrack calls this linear scan, so the existing 65,536-iteration ReDoS cap no longer bounds matching work as it did with the constant-time bitset lookup. For example, a class containing 100,000 repeated nonmatching members and a 65,536-byte subject can trigger billions of range comparisons; merge duplicate/overlapping ranges or otherwise cap/index them so crafted patterns cannot multiply the backtracking limit by the class length.

Useful? React with 👍 / 👎.

static_cast<std::uint32_t>(hi))}
};
}
ranges.push_back({*pending, hi});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prevent malformed range endpoints from spanning Unicode

When one endpoint is an invalid UTF-8 byte, its synthetic value above 0x10FFFF participates in ordinary range ordering, so a malformed pattern such as [a-\x80] unexpectedly matches every valid code point from a through U+10FFFF (including ). This contradicts the new byte-fallback contract that an invalid byte only compares equal to itself; reject ranges mixing synthetic and Unicode atoms or give malformed endpoints byte-only range semantics.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant